use std::ffi::{CStr, CString};

use libc::{c_void, c_int};

use lua_rs::core::*;
use lua_rs::ffi::*;

#[derive(Default, Debug)]
struct Point2D {
    x: f32,
    y: f32
}
impl Point2D {
    pub fn get(&self, name: &String) -> f32 {
        match name.as_str() {
            "x" => self.x,
            "y" => self.y,
            _ => unreachable!()
        }
    }        
    pub fn set(&mut self, name: &String, value: f32) {
        match name.as_str() {
            "x" => self.x = value,
            "y" => self.y = value,
            _ => unreachable!()
        }
    }
    pub fn _getter(self_instance: *const Self, getter_str: &String) -> f32 {
        unsafe {
            let self_ref = &*self_instance;
            match getter_str.as_str() {
                "x" => self_ref.x,
                "y" => self_ref.y,
                _ => unreachable!()
            }
        }
    }
}

unsafe extern "C" fn get_distributor(state: *mut lua_State) -> c_int {
    // println!("[Rust] In get_distributor");
    // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR |                            PARAMS FROM LUA
    // lua_dump_stack(state);

    let index_str = CStr::from_ptr(lua_tolstring(state, -1, std::ptr::null_mut())).to_owned();
    lua_settop(state, 1);
    // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE |                                        GET ACCESS INDEX_STR

    let object_address;
    match lua_type(state, lua_gettop(state)) {
        2 => {
            object_address = lua_touserdata(state, -1) as usize as i64;
        },
        5 => {
            let c_string = CString::new("__ObjectAddress").expect("");

            lua_pushstring(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | __ObjectAddress |              SET ACCESS TABLE INDEX_STR
            lua_gettable(state, -2);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | TABLE.__ObjectAddress |        FIND TABLE.__ObjectAddress
            object_address = lua_tointegerx(state, -1, std::ptr::null_mut());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | TABLE.__ObjectAddress |        GET VALUE TABLE.__ObjectAddress
            lua_settop(state, 1);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE |                                POP VALUE TABLE.__ObjectAddress
        },
        _ => unreachable!()
    }
    // Able to Find a Address
    if object_address != 0 {
        // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);

        let instance = object_address as *const Point2D;
        let instance_ref = &*instance;
        let get_value = instance_ref.get(&index_str.to_str().expect("!!!").to_string());
        // println!("[Rust] Get Point2D.{:?}, {}", &index_str, get_value);

        lua_settop(state, 1);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                            CLEAR
        lua_pushnumber(state, get_value.into());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| get_value |                                PUSH RESULT

        return 1
    }
    
    0
}

unsafe extern "C" fn set_distributor(state: *mut lua_State) -> c_int {
    // println!("[Rust] In set_distributor");

    // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE |                           PARAMS FROM LUA

    let index_str = CStr::from_ptr(lua_tolstring(state, 2, std::ptr::null_mut())).to_owned();
    
    let object_address;
    match lua_type(state, 1) {
        2 => {
            object_address = lua_touserdata(state, 1) as usize as i64;
        },
        5 => {
            let c_string = CString::new("__ObjectAddress").expect("");

            lua_pushstring(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | __ObjectAddress |              SET ACCESS TABLE INDEX_STR
            lua_gettable(state, 1);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | TABLE.__ObjectAddress |        FIND TABLE.__ObjectAddress
            object_address = lua_tointegerx(state, -1, std::ptr::null_mut());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | TABLE.__ObjectAddress |        GET VALUE TABLE.__ObjectAddress
        
            lua_settop(state, -2);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE |                                POP VALUE TABLE.__ObjectAddress
        },
        _ => unreachable!()
    }

    if object_address != 0 {
        // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);    
        
        let value = lua_tonumberx(state, 3, std::ptr::null_mut());
        // println!("Stack Num: {}", lua_gettop(state));             //  |·····| TABLE | INDEX_STR | VALUE |                                POP VALUE TABLE.__ObjectAddress
        
        let instance = object_address as *mut Point2D;
        let instance_ref = &mut *instance;
        instance_ref.set(&index_str.to_str().expect("!!!").to_string(), value as f32);
        // println!("[Rust] Set Point2D.{:?}, {}", &index_str, value);
    }

    // Deal Lua Logic
    // lua_rawset(state, 1);
    // lua_settop(state, -2);
    lua_settop(state, 0);

    0
}

unsafe fn lua_dump_stack(state: *mut lua_State) {
    let stack_top = lua_gettop(state);

    for i in 1..=stack_top {
        let element_type = lua_type(state, i);
        let type_str;
        let output;
        match element_type {
            0 => {
                output = CString::new("nil").unwrap();
                type_str = String::from("nil            ");
            },
            1 => {
                output = CString::new(lua_toboolean(state, i).to_string()).unwrap();
                type_str = String::from("boolean        ");
            },
            2 => {
                output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                type_str = String::from("lightuserdata  ");
            },
            3 => {
                let is_integer = lua_isinteger(state, i) != 0;
                if is_integer {
                    output = CString::new(lua_tointegerx(state, i, std::ptr::null_mut()).to_string() + "_" + &is_integer.to_string()).unwrap();
                } else {
                    output = CString::new(lua_tonumberx(state, i, std::ptr::null_mut()).to_string() + "_" + &is_integer.to_string()).unwrap();
                }
                type_str = String::from("number         ");
            },
            4 => {
                output = CStr::from_ptr(lua_tolstring(state, i, std::ptr::null_mut())).to_owned();
                type_str = String::from("string         ");
            },
            5 => {
                output = CString::new("todo").unwrap();
                type_str = String::from("table          ");
            },
            6 => {
                output = CString::new("todo").unwrap();
                type_str = String::from("function       ");
            },
            7 => {
                output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                type_str = String::from("userdata       ");
            },
            8 => {
                output = CString::new("todo").unwrap();
                type_str = String::from("thread         ");
            },
            _ => {
                output = CString::new("unknown").unwrap();
                type_str = String::from("unknown        ");
            }
        }
        println!("Stack Index {}[{:?}]: {:?}", i, type_str, output);
    }
    println!("");
}

fn raw_lua_test(c_str: &CString) {
    unsafe {
        let state = luaL_newstate();
        // luaopen_base(state);
        // luaopen_table(state);
        // luaopen_package(state);
        // luaopen_io(state);
        // luaopen_string(state);

        luaL_openlibs(state);

        // println!("Stack Num: {}", lua_gettop(state));
        lua_dump_stack(state);

        // -------------------- Add Int Value
        lua_pushinteger(state, 64);
        // println!("Stack Num: {}", lua_gettop(state));
        let c_string = CString::new("MyInt").expect("");

        lua_setglobal(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));

        // -------------------- Add Table Value
        let mut object = Point2D::default();
        object.x = 10.0;
        object.y = 20.0;

        let object_address = &object as *const Point2D as usize as i64;
        println!("rust object address: {:?}", std::ptr::addr_of!(object));

        lua_createtable(state, 0, 0);   
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           PUSH A TABLE
        lua_pushinteger(state, object_address);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | object_address |          PUSH A INTEGER
        let c_string = CString::new("__ObjectAddress").unwrap();
        lua_setfield(state, -2, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           SET TABLE.__ObjectAddress = INTEGER; POP INTEGER

        let c_string = CString::new("MyMetatable").unwrap();
        luaL_newmetatable(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table |              PUSH A METATBABLE
        lua_pushcclosure(state, Some(get_distributor), 0);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table | function |   PUSH A METATBABLE
        let c_string = CString::new("__index").unwrap();
        lua_setfield(state, -2, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table |              SET METATBLE.__index = FUNCTION; POP FUNCTION
        lua_settop(state, -2);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           POP METATABLE

        let c_string = CString::new("MyMetatable").unwrap();
        luaL_setmetatable(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           SET TABLE.METATABLE = MyMetatable
        let c_string = CString::new("MyTable").unwrap();
        lua_setglobal(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                   SET GLOBAL.MyTable = TABLE; POP TABLE

        // -------------------- Add LightUserdata
        lua_pushlightuserdata(state, &mut object as *mut Point2D as *mut c_void);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| light_userdata |                  CREATE LIGHT USERDATA
        let c_string = CString::new("MyMetatable").unwrap();
        luaL_setmetatable(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| light_userdata |                  SET LIGHTUSERDATA.METATABLE = MyMetatable
        let c_string = CString::new("MyLightUserdata").unwrap();
        lua_setglobal(state, c_string.as_ptr());
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                   SET GLOBAL.MyLightUserdata = LIGHTUSERDATA; POP TABLE

        luaL_dostring(state, c_str.as_ptr());
    }
}

fn pack_lua_test(c_str: &CString) {
    let lua_engine = LuaEngine::new();

    lua_engine.dump_stack();
    lua_engine.set_global_integer("MyInt", &64);

    // Arrange Object Data
    let mut object = Point2D::default();
    object.x = 10.0;
    object.y = 20.0;
    let object_address = &object as *const Point2D as usize as i64;

    println!("[Rust] {:?}", object);

    // Create Metatable
    lua_engine.create_metatable("MyMetatable");
    lua_engine.set_table_cfunction("__index", &Some(get_distributor), &0);
    lua_engine.set_table_cfunction("__newindex", &Some(set_distributor), &0);
    lua_engine.pop_stack(&1);

    // Table Strategy
    lua_engine.create_table();
    lua_engine.set_table_integer("__ObjectAddress", &object_address, &0);
    lua_engine.set_metatable("MyMetatable", &0);
    lua_engine.set_global_value("MyTable", &0);

    // LightUserdata Strategy
    lua_engine.push_lightuserdata(&mut object as *mut Point2D as *mut c_void);
    lua_engine.set_metatable("MyMetatable", &0);
    lua_engine.set_global_value("MyLightUserdata", &0);

    lua_engine.run_script(c_str);

    println!("[Rust] {:?}", object);
}

fn main() {

    let c_str = CString::new(
        r#"
print("[During Lua] START")
print(MyInt)
print(MyTable)
print(MyLightUserdata)

print("[    ] Point2D.x:", MyTable.x);
print("[    ] Point2D.y:", MyTable.y);

MyTable.x = MyTable.x * 20.0;
MyLightUserdata.y = MyLightUserdata.y / 10.0;

print("[    ] Point2D.x:", MyLightUserdata.x);
print("[    ] Point2D.y:", MyLightUserdata.y);

print("[    ] lua light userdata: ", MyLightUserdata)
print("[    ] lua object address: ", string.format("0x%x", MyTable.__ObjectAddress))

print("[During Lua] END")
        "#
    ).unwrap();

    // raw_lua_test(&c_str);
    pack_lua_test(&c_str);
}